package repository

import (
	"context"
	"database/sql"

	"gitee.com/hp-huiw/family-expense-api/internal/db"
	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	jet "github.com/go-jet/jet/v2/mysql"
)

type BankCard struct {
	ID                    string  `db:"id"`
	Name                  string  `db:"name"`
	IsCreditCard          bool    `db:"is_credit_card"`
	Enabled               bool    `db:"enabled"`
	StatementDate         *string `db:"statement_date"`
	PaymentDueDate        *string `db:"payment_due_date"`
	CustomerServiceNumber *string `db:"customer_service_number"`
	BankID                *string `string:"bank_id"`
	BankName              *string `string:"bank_name"`
	BankShortName         *string `string:"bank_short_name"`
}

type BankCardRepository interface {
	Count(ctx context.Context, accountGroupID string) (int64, error)
	QueryAllWithBasicInfo(ctx context.Context, accountGroupID string) ([]BankCard, error)
	QueryBankCardInfo(ctx context.Context, accountGroupID string, id string) (*BankCard, error)
	QueryBasicInfo(ctx context.Context, accountGroupID string, pageSize *int64, pageIndex *int64) ([]BankCard, error)
	QueryIDs(ctx context.Context, ids []string, accountGroupID string) ([]string, error)
	Insert(ctx context.Context, model *dbmodel.BankCard) error
	Update(ctx context.Context, accountGroupID string, id string, model *dbmodel.BankCard) error
	Delete(ctx context.Context, accountGroupID string, id string) error
	BatchDelete(ctx context.Context, accountGroupID string, ids []string) error
	DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

func NewBankCardRepository(db *sql.DB) BankCardRepository {
	return &bankCardRepository{db: db}
}

// Count counts the bank card by accountGroupID.
func (r *bankCardRepository) Count(ctx context.Context, accountGroupID string) (int64, error) {
	return db.Count(ctx, r.db, tbBankCard, tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)))
}

// QueryAllWithBasicInfo queries all bank cards with basic info which associated with provided account group.
func (r *bankCardRepository) QueryAllWithBasicInfo(ctx context.Context, accountGroupID string) ([]BankCard, error) {
	return r.QueryBasicInfo(ctx, accountGroupID, nil, nil)
}

// QueryBankCardInfo queries bank card based on provided account group id and id.
func (r *bankCardRepository) QueryBankCardInfo(ctx context.Context, accountGroupID string, id string) (*BankCard, error) {
	var query struct {
		ID                    string  `db:"id"`
		Name                  string  `db:"name"`
		IsCreditCard          bool    `db:"is_credit_card"`
		Enabled               bool    `db:"enabled"`
		StatementDate         *string `db:"statement_date"`
		PaymentDueDate        *string `db:"payment_due_date"`
		CustomerServiceNumber *string `db:"customer_service_number"`
		BankID                *string `string:"bank_id"`
		BankName              *string `string:"bank_name"`
		BankShortName         *string `string:"bank_short_name"`
	}

	stmt := jet.
		SELECT(
			tbBankCard.ID.AS("id"),
			tbBankCard.Name.AS("name"),
			tbBankCard.IsCreditCard.AS("is_credit_card"),
			tbBankCard.Enabled.AS("enabled"),
			tbBankCard.StatementDate.AS("statement_date"),
			tbBankCard.PaymentDueDate.AS("payment_due_date"),
			tbBankCard.CustomerServiceNumber.AS("customer_service_number"),
			tbBank.ID.AS("bank_id"),
			tbBank.ShortName.AS("bank_short_name"),
			tbBank.Name.AS("bank_name"),
		).
		FROM(
			tbBankCard.
				INNER_JOIN(tbBank, tbBank.ID.EQ(tbBankCard.BankID)),
		).
		WHERE(
			tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)).
				AND(tbBankCard.ID.EQ(jet.String(id))),
		)

	err := stmt.QueryContext(ctx, r.db, &query)

	if err != nil {
		return nil, err
	}

	return (*BankCard)(&query), nil
}

// QueryBasicInfo queries all bank cards basic info which associated with provided account group.
func (r *bankCardRepository) QueryBasicInfo(ctx context.Context, accountGroupID string, pageSize *int64, pageIndex *int64) ([]BankCard, error) {
	var query []struct {
		ID                    string  `db:"id"`
		Name                  string  `db:"name"`
		IsCreditCard          bool    `db:"is_credit_card"`
		Enabled               bool    `db:"enabled"`
		StatementDate         *string `db:"statement_date"`
		PaymentDueDate        *string `db:"payment_due_date"`
		CustomerServiceNumber *string `db:"customer_service_number"`
		BankID                *string `string:"bank_id"`
		BankName              *string `string:"bank_name"`
		BankShortName         *string `string:"bank_short_name"`
	}

	stmt := jet.
		SELECT(
			tbBankCard.ID.AS("id"),
			tbBankCard.Name.AS("name"),
			tbBankCard.IsCreditCard.AS("is_credit_card"),
			tbBankCard.Enabled.AS("enabled"),
			tbBankCard.StatementDate.AS("statement_date"),
			tbBankCard.PaymentDueDate.AS("payment_due_date"),
			tbBank.ID.AS("bank_id"),
			tbBank.Name.AS("bank_name"),
			tbBank.ShortName.AS("bank_short_name"),
		).
		FROM(
			tbBankCard.
				INNER_JOIN(tbBank, tbBank.ID.EQ(tbBankCard.BankID)),
		).
		WHERE(
			tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)),
		).
		ORDER_BY(
			tbBankCard.BankID.ASC(),
		)

	if pageSize != nil && pageIndex != nil {
		stmt = stmt.
			LIMIT(*pageSize).
			OFFSET(*pageSize * *pageIndex)
	}

	if err := stmt.QueryContext(ctx, r.db, &query); err != nil {
		return nil, err
	}

	result := make([]BankCard, 0, len(query))

	for _, item := range query {
		result = append(result, item)
	}

	return result, nil
}

// QueryIDs queries the input id list and makes sure they belong to the specific account group.
func (r *bankCardRepository) QueryIDs(ctx context.Context, ids []string, accountGroupID string) ([]string, error) {
	query := []string{}

	if len(ids) > 0 {
		idsExpression := db.SliceExpression(ids)

		stmt := tbBankCard.
			SELECT(
				tbBankCard.ID,
			).
			FROM(
				tbBankCard,
			).
			WHERE(
				tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)).
					AND(tbBankCard.ID.IN(idsExpression...)),
			)

		if err := stmt.QueryContext(ctx, r.db, &query); err != nil {
			return nil, err
		}
	}

	return query, nil
}

// Insert inserts new bank card.
func (r *bankCardRepository) Insert(ctx context.Context, model *dbmodel.BankCard) error {
	stmt := tbBankCard.
		INSERT().
		MODEL(model)

	_, err := stmt.ExecContext(ctx, r.db)

	return err
}

// Update updates bank card info based on provided model data, account group id and its id.
func (r *bankCardRepository) Update(ctx context.Context, accountGroupID string, id string, model *dbmodel.BankCard) error {
	condition := tbBankCard.ID.EQ(jet.String(id)).
		AND(tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)))

	stmt := tbBankCard.
		UPDATE(
			tbBankCard.Name,
			tbBankCard.StatementDate,
			tbBankCard.PaymentDueDate,
			tbBankCard.IsCreditCard,
			tbBankCard.Enabled,
			tbBankCard.CustomerServiceNumber,
			tbBankCard.BankID,
			tbBankCard.LastUpdated,
		).
		MODEL(model).
		WHERE(condition)

	_, err := stmt.ExecContext(ctx, r.db)

	return err
}

// Delete deletes the BankCard with provided account group id and id.
func (r *bankCardRepository) Delete(ctx context.Context, accountGroupID string, id string) error {
	stmt := tbBankCard.
		DELETE().
		WHERE(
			tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)).
				AND(tbBankCard.ID.EQ(jet.String(id))),
		)

	_, err := stmt.ExecContext(ctx, r.db)

	return err
}

// BatchDelete deletes the BankCards with provides list of id.
func (r *bankCardRepository) BatchDelete(ctx context.Context, accountGroupID string, ids []string) error {
	if len(ids) > 0 {
		idsExpression := db.SliceExpression(ids)

		stmt := tbBankCard.
			DELETE().
			WHERE(
				tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)).
					AND(tbBankCard.ID.IN(idsExpression...)),
			)

		_, err := stmt.ExecContext(ctx, r.db)

		return err
	}

	return nil
}

// DeleteWithTx deletes all bank cards by account group id with transaction.
func (*bankCardRepository) DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	stmt := tbBankCard.
		DELETE().
		WHERE(
			tbBankCard.AccountGroupID.EQ(jet.String(accountGroupID)),
		)

	_, err := stmt.ExecContext(ctx, tx)

	return err
}

type bankCardRepository struct {
	db *sql.DB
}
